#include <jni.h>
#include <android/log.h>
#include <stdio.h>
#include <string.h>
#include <mediastreamer2/msjava.h>
#include <belle-sip/wakelock.h>
#include "com_smartphone_core_Smartphone.h"
#include "smartphonecore.h"

static const char *TAG = "smartphonecore-jni";
static const char *LOG_DOMAIN = "smartphonecore";
#define Logd(...)  __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)

extern "C" {
static void smartphone_log_func(SmartphoneLogLevel level, const char *fmt, va_list args);
static void smartphone_callback(SmartphoneCallbackParam *param);
}

JavaVM *g_jvm = nullptr;
jclass cls_smartphone = nullptr;
jobject obj_smartphone = nullptr;

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
    g_jvm = jvm;
    ms_set_jvm(g_jvm);
    return JNI_VERSION_1_2;
}


static void smartphone_log_func_print(int prio, char *str) {
    char *current;
    char *next;

    if (strlen(str) < 512) {
        __android_log_write(prio, LOG_DOMAIN, str);
    } else {
        current = str;
        while ((next = strchr(current, '\n')) != NULL) {

            *next = '\0';
            if (next != str && next[-1] == '\r')
                next[-1] = '\0';
            __android_log_write(prio, LOG_DOMAIN, current);
            current = next + 1;
        }
        __android_log_write(prio, LOG_DOMAIN, current);
    }
}

static void smartphone_log_func(SmartphoneLogLevel lev, const char *fmt, va_list args) {
    char str[4096];
    vsnprintf(str, sizeof(str) - 1, fmt, args);
    str[sizeof(str) - 1] = '\0';
    int prio;
    switch (lev) {
        case SMARTPHONE_DEBUG:
            prio = ANDROID_LOG_DEBUG;
            break;
        case SMARTPHONE_MESSAGE:
            prio = ANDROID_LOG_INFO;
            break;
        case SMARTPHONE_WARNING:
            prio = ANDROID_LOG_WARN;
            break;
        case SMARTPHONE_ERROR << 4:
            prio = ANDROID_LOG_ERROR;
            break;
        case SMARTPHONE_FATAL << 5:
            prio = ANDROID_LOG_FATAL;
            break;
        default:
            prio = ANDROID_LOG_DEFAULT;
            break;
    }

    smartphone_log_func_print(prio, str);
}

static void smartphone_callback(SmartphoneCallbackParam *param) {
    JNIEnv *env = nullptr;
    jint result = g_jvm->AttachCurrentThread(&env, nullptr);
    if (result != 0) {
        Logd("cannot attch VM");
        return;
    }
    switch (param->state) {
        case SmartphoneRegistrationNone:
        case SmartphoneRegistrationProgress:
        case SmartphoneRegistrationOk:
        case SmartphoneRegistrationCleared:
        case SmartphoneRegistrationFailed: {
            RegistrationStateInfo *regInfo = (RegistrationStateInfo *) param->data;

            jstring proxy = regInfo->proxy ? env->NewStringUTF(regInfo->proxy) : nullptr;
            jstring message = regInfo->message ? env->NewStringUTF(regInfo->message) : nullptr;
            jmethodID jid = env->GetMethodID(cls_smartphone, "onRegistrationStateChanged",
                                             "(ILjava/lang/String;Ljava/lang/String;)V");
            if (jid == nullptr) {
                Logd("cannot find method id for onRegistrationStateChanged");
                break;
            }
            env->CallVoidMethod(obj_smartphone, jid, (jint) param->state, proxy, message);
            if (proxy) env->DeleteLocalRef(proxy);
            if (message) env->DeleteLocalRef(message);
        }
            break;
        case SmartphoneCallIncomingReceived:
        case SmartphoneCallIdle:
        case SmartphoneCallOutgoingInit:
        case SmartphoneCallOutgoingProgress:
        case SmartphoneCallOutgoingRinging:
        case SmartphoneCallOutgoingEarlyMedia:
        case SmartphoneCallConnected:
        case SmartphoneCallStreamsRunning:
        case SmartphoneCallPausing:
        case SmartphoneCallPaused:
        case SmartphoneCallResuming:
        case SmartphoneCallRefered:
        case SmartphoneCallError:
        case SmartphoneCallEnd:
        case SmartphoneCallPausedByRemote:
        case SmartphoneCallUpdatedByRemote:
        case SmartphoneCallIncomingEarlyMedia:
        case SmartphoneCallUpdating:
        case SmartphoneCallReleased:
        case SmartphoneCallEarlyUpdatedByRemote:
        case SmartphoneCallEarlyUpdating: {
            CallStateInfo *callInfo = (CallStateInfo *) param->data;
            long callid = callInfo->callid;
            Logd("dispname = %s", callInfo->dispname);
            Logd("username = %s", callInfo->username);
            Logd("message = %s", callInfo->message);

            jstring dispname = callInfo->dispname ? env->NewStringUTF(callInfo->dispname) : nullptr;
            jstring username = callInfo->username ? env->NewStringUTF(callInfo->username) : nullptr;
            jstring message = callInfo->message ? env->NewStringUTF(callInfo->message) : nullptr;
            jmethodID jid = env->GetMethodID(cls_smartphone, "onCallStateChanged",
                                             "(IJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");

            if (jid == nullptr) {
                Logd("cannot find method id for onRegistrationStateChanged");
                break;
            }

            env->CallVoidMethod(obj_smartphone, jid, (jint) param->state, (jlong) callid, dispname,
                                username, message);
            if (dispname) env->DeleteLocalRef(dispname);
            if (username) env->DeleteLocalRef(username);
            if (message) env->DeleteLocalRef(message);
        }
            break;
        case SmartphoneCallInfoReceived:
            CallInfo *callInfo = (CallInfo *) param->data;
            long callid = callInfo->callid;
            jstring info = callInfo->info ? env->NewStringUTF(callInfo->info) : nullptr;

            jmethodID jid = env->GetMethodID(cls_smartphone, "onCallInfoReceived",
                                             "(JLjava/lang/String;)V");
            if (jid == nullptr) {
                Logd("cannot find method id for onCallInfoReceived");
                break;
            }
            env->CallVoidMethod(obj_smartphone, jid, (jlong) callid, info);

            if (info) env->DeleteLocalRef(info);
            break;
    }
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_iterate(JNIEnv *, jobject) {
    smartphone_iterate();
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_init(JNIEnv *env, jobject thiz) {
    obj_smartphone = env->NewGlobalRef(thiz);
    jclass clazz = env->GetObjectClass(thiz);
    cls_smartphone = (jclass) env->NewGlobalRef(clazz);
    smartphone_init(smartphone_callback, smartphone_log_func);

}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_uninit(JNIEnv *env, jobject) {
    smartphone_uninit();
    env->DeleteGlobalRef(cls_smartphone);
    env->DeleteGlobalRef(obj_smartphone);
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_register
        (JNIEnv *env, jobject, jstring server, jint portType, jstring dispname, jstring username,
         jstring passwd, jint expires) {

    const char *cserver = server ? env->GetStringUTFChars(server, nullptr) : nullptr;
    const char *cdispname = dispname ? env->GetStringUTFChars(dispname, nullptr) : nullptr;
    const char *cusername = username ? env->GetStringUTFChars(username, nullptr)  : nullptr;
    const char *cpasswd = passwd ? env->GetStringUTFChars(passwd, nullptr) : nullptr;
    smartphone_register(cserver, (SmartphoneTransportType) portType, cdispname, cusername, cpasswd,
                        expires);

    if (dispname) env->ReleaseStringUTFChars(dispname, cdispname);
    if(username) env->ReleaseStringUTFChars(username, cusername);
    if(passwd) env->ReleaseStringUTFChars(passwd, cpasswd);
    if(server) env->ReleaseStringUTFChars(server, cserver);
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_unregister(JNIEnv *, jobject) {
    smartphone_unregister();
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_refreshRegisters(JNIEnv *, jobject) {
    smartphone_refresh_registers();
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_call
        (JNIEnv *env, jobject, jstring identity, jint mt) {
    const char *cidentity = env->GetStringUTFChars(identity, nullptr);
    smartphone_call(cidentity, (MediaType) mt);
    env->ReleaseStringUTFChars(identity, cidentity);
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_answer
        (JNIEnv *, jobject, jlong callid, jint mt) {
    smartphone_answer((long) callid, (MediaType) mt);
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_terminate
        (JNIEnv *, jobject, jlong callid) {
    smartphone_terminate((long) callid);
}

JNIEXPORT jint JNICALL Java_com_smartphone_core_Smartphone_getCallsNb
        (JNIEnv *env, jobject thiz) {
    return (jint) smartphone_get_calls_nb();
}

JNIEXPORT jboolean JNICALL Java_com_smartphone_core_Smartphone_callVideoEnabled
        (JNIEnv *, jobject, jlong callid) {
    jboolean ret = (jboolean) smartphone_call_video_enabled((long) callid);
    return ret;
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_setRemoteVideoWindow
        (JNIEnv *env, jobject, jobject window) {
    jobject oldWindow = (jobject) smartphone_get_native_window_id();
    if (window != NULL) {
        window = env->NewGlobalRef(window);
    }
    smartphone_set_native_window_id((long) window);
    if (oldWindow != NULL) {
        env->DeleteGlobalRef(oldWindow);
    }
}


JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_setLocalVideoWindow
        (JNIEnv *env, jobject, jobject window) {
    jobject oldWindow = (jobject) smartphone_get_native_preview_window_id();
    if (window != NULL) {
        window = env->NewGlobalRef(window);
    }
    smartphone_set_native_preview_window_id((long) window);
    if (oldWindow != NULL) {
        env->DeleteGlobalRef(oldWindow);
    }
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_setCameraRotation
        (JNIEnv *, jobject, jint) {

}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_sendInfo
        (JNIEnv *env, jobject thiz, jstring info, jlong callid) {
    const char *cinfo = env->GetStringUTFChars(info, nullptr);
    smartphone_send_info(cinfo, (long) callid);
    env->ReleaseStringUTFChars(info, cinfo);
}
JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_setStaticPicture
        (JNIEnv *env, jobject, jstring path){
    const char *cpath = env->GetStringUTFChars(path, nullptr);
    smartphone_set_static_picture(cpath);
    env->ReleaseStringUTFChars(path, cpath);
}
JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_setPowerManager
        (JNIEnv *env, jobject, jobject pm){
    if(pm != NULL) belle_sip_wake_lock_init(env, pm);
    else belle_sip_wake_lock_uninit(env);

}
JNIEXPORT jint JNICALL Java_com_smartphone_core_Smartphone_getCodecSize
        (JNIEnv *, jobject, jint type){
    return (jint)smartphone_get_codec_size((CodecType) type);
}

JNIEXPORT jstring JNICALL Java_com_smartphone_core_Smartphone_getCodecName
        (JNIEnv *env, jobject, jint type ,  jint id){
    const char *name = smartphone_get_codec_name((CodecType) type, (int) id);
    return env->NewStringUTF(name);
}

JNIEXPORT jint JNICALL Java_com_smartphone_core_Smartphone_getCodecClockRate
        (JNIEnv *, jobject, jint type, jint id){
    return (jint)smartphone_get_codec_clock_rate((CodecType) type, (int) id);
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_enableCodec
        (JNIEnv *, jobject, jint type, jint id, jboolean enabled){
    smartphone_enable_codec((CodecType) type, (int) id, (int) enabled);
}

JNIEXPORT jint JNICALL Java_com_smartphone_core_Smartphone_getVideoDeviceSize
        (JNIEnv *, jobject){
    return (jint)smartphone_get_video_device_size();
}

JNIEXPORT jstring JNICALL Java_com_smartphone_core_Smartphone_getVideoDeviceName
        (JNIEnv *env, jobject, jint id){
    const char *name = smartphone_get_video_device_name(id);
    return env->NewStringUTF(name);
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_setVideoDevice
        (JNIEnv *env, jobject, jstring name){
    if(!name) return;
    const char *cname = env->GetStringUTFChars(name, nullptr);
    smartphone_set_video_device(cname);
    env->ReleaseStringUTFChars(name, cname);
}

JNIEXPORT jint JNICALL Java_com_smartphone_core_Smartphone_getSupportedVideoSize
        (JNIEnv *, jobject){
    return (jint)smartphone_get_supported_video_size();
}

JNIEXPORT jstring JNICALL Java_com_smartphone_core_Smartphone_getSupportedVideoSizeName
        (JNIEnv *env, jobject, jint id){
    const char *name = smartphone_get_supported_video_size_name(id);
    return env->NewStringUTF(name);
}

JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_setPreferredVideoSize
        (JNIEnv *env, jobject, jstring name){
    if(!name) return;
    const char *cname = env->GetStringUTFChars(name, nullptr);
    smartphone_set_preferred_video_size(cname);
    env->ReleaseStringUTFChars(name, cname);
}
JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_setPrimaryContact
        (JNIEnv *env, jobject, jstring dispname, jstring username){
    const char *cdispname = dispname ? env->GetStringUTFChars(dispname, nullptr) : nullptr;
    const char *cusername = username ? env->GetStringUTFChars(username, nullptr)  : nullptr;

    smartphone_set_primary_contact(cdispname, cusername);

    if (dispname) env->ReleaseStringUTFChars(dispname, cdispname);
    if(username) env->ReleaseStringUTFChars(username, cusername);
}
JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_enableEchoCancellation
        (JNIEnv *, jobject, jboolean enabled){
    smartphone_enable_echo_cancellation((int) enabled);
}
JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_enableAdaptiveRrateControl
        (JNIEnv *, jobject, jboolean enabled){
    smartphone_enable_adaptive_rate_control((int)enabled);
}
JNIEXPORT void JNICALL Java_com_smartphone_core_Smartphone_setLocalPort
        (JNIEnv *, jobject, jint port){
    Logd("set local port = %d", port);
    smartphone_set_local_port((int) port);
}